package kz.pompei.ifacer.util;

import static java.lang.Math.min;
import static kz.pompei.ifacer.util.Util.eq;
import static kz.pompei.ifacer.util.Util.floatToStr;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

public class RichText {
  private RichTextDefStyle defStyle = new RichTextDefStyle();
  private Style style = new Style();
  
  private class Style implements Cloneable {
    boolean bold = false, italic = false, underlined = false, strikethrough = false;
    Color color = defStyle.color;
    String fontFamily = defStyle.fontFamily;
    float fontSize = defStyle.fontSize;
    
    Style copy() {
      try {
        return (Style)clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
      }
    }
    
    void appendDifference(StringBuilder sb, Style nextStyle) {
      if (nextStyle.bold != bold) sb.append('*');
      if (nextStyle.italic != italic) sb.append('/');
      if (nextStyle.underlined != underlined) sb.append('_');
      if (nextStyle.strikethrough != strikethrough) sb.append('~');
      if (!eq(nextStyle.color, color)) appendColor(sb, nextStyle.color);
      if (!eq(nextStyle.fontFamily, fontFamily)) {
        sb.append("{f").append(nextStyle.fontFamily);
        if (nextStyle.fontSize != fontSize) {
          sb.append('/').append(nextStyle.fontSize);
        }
        sb.append('}');
      } else if (nextStyle.fontSize != fontSize) {
        sb.append("{s").append(floatToStr(nextStyle.fontSize)).append('}');
      }
    }
    
    @Override
    public String toString() {
      return "{" + fontFamily + "/" + fontSize + " rgb(" + color.getRed() + ',' + color.getGreen()
          + ',' + color.getBlue() + ") " + (bold ? "B" :"") + (italic ? "I" :"")
          + (underlined ? "U" :"") + (strikethrough ? "S" :"") + "}";
    }
    
    public void apply(String richText, int from, int to) {
      if (to == from) to++;
      String marker = richText.substring(from, to);
      if ("*".equals(marker)) {
        bold = !bold;
        return;
      }
      if ("/".equals(marker)) {
        italic = !italic;
        return;
      }
      if ("~".equals(marker)) {
        strikethrough = !strikethrough;
        return;
      }
      if ("_".equals(marker)) {
        underlined = !underlined;
        return;
      }
      if (marker.toLowerCase().startsWith("{f")) {
        String[] split = marker.substring(2).trim().split("/");
        if (split.length <= 0) return;
        if (split.length >= 1) {
          fontFamily = split[0].trim();
        }
        if (split.length >= 2) {
          try {
            fontSize = Float.parseFloat(split[1].trim());
          } catch (NumberFormatException e) {}
        }
        return;
      }
      if (marker.toLowerCase().startsWith("{s")) {
        try {
          fontSize = Float.parseFloat(marker.substring(2).trim());
        } catch (NumberFormatException e) {}
        return;
      }
      if (marker.toLowerCase().startsWith("{c")) {
        color = Util.strToColor(marker.substring(2).trim());
        return;
      }
    }
    
    public void assignTo(Graphics g, boolean colored) {
      g.setFont(new Font(fontFamily, (bold ? 1 :0) * Font.BOLD + (italic ? 1 :0) * Font.ITALIC, 1)
          .deriveFont(fontSize));
      if (colored && color != null) g.setColor(color);
    }
  }
  
  static class Part {
    final Style style;
    final String content;
    
    public Part(Style style, String content) {
      this.style = style.copy();
      this.content = killAllBackslashes(content);
    }
    
    @Override
    public String toString() {
      return style + "[" + content + "]";
    }
  }
  
  final List<Part> parts = new ArrayList<>();
  
  public void print(PrintStream out) {
    for (Part part : parts) {
      out.println(part);
    }
  }
  
  private RichText() {}
  
  public RichText(RichTextDefStyle defStyle) {
    this.defStyle = defStyle;
  }
  
  static void fillIndexes(List<Integer> openIndexes, List<Integer> closeIndexes, String richText) {
    boolean backslash = false;
    int deep = 0;
    for (int i = 0, C = richText.length(); i < C; i++) {
      char c = richText.charAt(i);
      
      if (c == '\\') {
        backslash = !backslash;
        continue;
      }
      
      if (backslash) {
        backslash = false;
        continue;
      }
      
      switch (c) {
      case '{':
        if (deep == 0) openIndexes.add(i);
        deep++;
        continue;
        
      case '}':
        if (deep == 1) closeIndexes.add(i);
        deep--;
        continue;
        
      case '*':
      case '~':
      case '/':
      case '_':
        if (deep == 0) {
          openIndexes.add(i);
          closeIndexes.add(i);
        }
        continue;
        
      default:
        continue;
      }
    }
  }
  
  public static RichText parse(String richText) {
    RichText ret = new RichText();
    ret.append(richText);
    return ret;
  }
  
  public static RichText parse(RichTextDefStyle defStyle, String richText) {
    RichText ret = new RichText(defStyle);
    ret.append(richText);
    return ret;
  }
  
  public RichText append(String richText) {
    final List<Integer> openIndexes = new ArrayList<>();
    final List<Integer> closeIndexes = new ArrayList<>();
    
    fillIndexes(openIndexes, closeIndexes, richText);
    int C = min(openIndexes.size(), closeIndexes.size());
    
    if (C == 0) {
      parts.add(new Part(new Style(), richText));
      return this;
    }
    {
      style = new Style();
      if (openIndexes.get(0) > 0) {
        parts.add(new Part(style, richText.substring(0, openIndexes.get(0))));
      }
      for (int i = 0; i < C; i++) {
        style.apply(richText, openIndexes.get(i), closeIndexes.get(i));
        final String content;
        if (i < C - 1) {
          content = richText.substring(closeIndexes.get(i) + 1, openIndexes.get(i + 1));
        } else {
          content = richText.substring(closeIndexes.get(i) + 1);
        }
        if (content != null && content.length() > 0) {
          parts.add(new Part(style, content));
        }
      }
      return this;
    }
  }
  
  private static void appendColor(StringBuilder sb, Color color) {
    if (color == null) return;
    sb.append("{crgb ").append(color.getRed()).append(',');
    sb.append(color.getGreen()).append(',').append(color.getBlue()).append('}');
  }
  
  public String compile() {
    StringBuilder sb = new StringBuilder();
    Style current = new Style();
    for (Part part : parts) {
      current.appendDifference(sb, part.style);
      current = part.style;
      appendQuoted(sb, part.content);
    }
    return sb.toString();
  }
  
  private static void appendQuoted(StringBuilder sb, String str) {
    if (str == null) return;
    for (int i = 0, C = str.length(); i < C; i++) {
      char c = str.charAt(i);
      switch (c) {
      case '~':
      case '/':
      case '*':
      case '\\':
      case '_':
        sb.append('\\');
      default:
        sb.append(c);
      }
    }
  }
  
  private static String killAllBackslashes(String content) {
    if (content == null) return null;
    StringBuilder sb = new StringBuilder();
    boolean wasBackslash = false;
    for (int i = 0, C = content.length(); i < C; i++) {
      char c = content.charAt(i);
      if (c == '\\' && !wasBackslash) {
        wasBackslash = true;
        continue;
      }
      sb.append(c);
      wasBackslash = false;
    }
    return sb.toString();
  }
  
  public TextSize getSize(Graphics g) {
    int ascent = 0, descent = 0, leading = 0;
    int width = 0;
    for (Part part : parts) {
      part.style.assignTo(g, true);
      FontMetrics fm = g.getFontMetrics();
      width += fm.stringWidth(part.content);
      int a = fm.getAscent();
      int d = fm.getDescent();
      int l = fm.getLeading();
      if (ascent < a) ascent = a;
      if (descent < d) descent = d;
      if (leading < l) leading = l;
    }
    return new TextSize(ascent, descent, leading, width);
  }
  
  public void draw(Graphics g, int x, int y, boolean colored) {
    for (Part part : parts) {
      part.style.assignTo(g, colored);
      g.drawString(part.content, x, y);
      int width = g.getFontMetrics().stringWidth(part.content);
      if (part.style.underlined) {
        g.drawLine(x, y + 1, x + width, y + 1);
      }
      if (part.style.strikethrough) {
        int offset = g.getFontMetrics().getAscent() / 4 + 2;
        g.drawLine(x, y - offset, x + width, y - offset);
      }
      x += width;
    }
  }
  
  public void setFontFamilyEverywhere(String fontFamily) {
    for (Part part : parts) {
      part.style.fontFamily = fontFamily;
    }
  }
  
  public void setFontSizeEverywhere(float fontSize) {
    for (Part part : parts) {
      part.style.fontSize = fontSize;
    }
  }
  
  public String getFirstFontFamily() {
    if (parts.size() == 0) return defStyle.fontFamily;
    return parts.get(0).style.fontFamily;
  }
  
  public float getFirstFontSize() {
    if (parts.size() == 0) return defStyle.fontSize;
    return parts.get(0).style.fontSize;
  }
  
  public Color getFirstColor() {
    if (parts.size() == 0) return defStyle.color;
    return parts.get(0).style.color;
  }
  
  public void setColorEverywhere(Color color) {
    for (Part part : parts) {
      part.style.color = color;
    }
  }
}